package com.partsoft.dits.ldap;

import java.sql.Types;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.naming.CommunicationException;
import javax.naming.ServiceUnavailableException;


import org.springframework.jdbc.CannotGetJdbcConnectionException;
import org.springframework.ldap.core.ContextSource;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import com.partsoft.dits.DataChangeLog;
import com.partsoft.dits.LdapDataEdgeDescriptor;
import com.partsoft.dits.MonitorStatus;
import com.partsoft.dits.log.ChangeLogResolver;
import com.partsoft.dits.log.ChangeLogStorage;
import com.partsoft.dits.message.DataAction;
import com.partsoft.dits.message.DataProperty;
import com.partsoft.dits.utils.EdgeLogUtils;
import com.partsoft.dits.utils.ExceptionUtils;
import com.partsoft.dits.utils.LdapUtils;

public class LdapChangeLogMonitorTask implements Runnable {

	private LdapChangeLogMonitorContext monitorContext;

	private MonitorStatus monitorStatus = MonitorStatus.STOPPED;

	private long monitorInterval = 5;

	private int retrieveCount = 150;

	private LdapChangeLogMonitorDescriptor monitorDescriptor;

	LdapChangeLogMonitorTask() {
	}

	LdapChangeLogMonitorTask(LdapChangeLogMonitorDescriptor desc, LdapChangeLogMonitorContext context) {
		setMonitorDescriptor(desc);
		setMonitorInterval(desc.getMonitorInterval());
		setRetrieveCount(desc.getRetrieveCount());
		setMonitorContext(context);
	}

	public LdapChangeLogMonitorDescriptor getMonitorDescriptor() {
		return monitorDescriptor;
	}

	public void setMonitorDescriptor(LdapChangeLogMonitorDescriptor monitorDescriptor) {
		this.monitorDescriptor = monitorDescriptor;
	}

	public int getRetrieveCount() {
		return retrieveCount;
	}

	public void setRetrieveCount(int retrieveCount) {
		this.retrieveCount = retrieveCount;
	}

	public long getMonitorInterval() {
		return monitorInterval;
	}

	public void setMonitorInterval(long monitorInterval) {
		this.monitorInterval = monitorInterval;
	}

	public void run() {
		setMonitorStatus(MonitorStatus.STARTED);
		boolean errorFocus = false;
		while (getMonitorStatus() != MonitorStatus.STOPPED) {
			if (getMonitorStatus() == MonitorStatus.PAUSED) {
				try {
					Thread.sleep(1000);
				} catch (Throwable e) {
				}
				continue;
			}
			try {
				Thread.sleep(1000 * getMonitorInterval());
			} catch (Exception e) {
			}
			if (getMonitorStatus() == MonitorStatus.STOPPED || getMonitorStatus() == MonitorStatus.PAUSED)
				continue;
			synchronized (this) {
				try {
					doMonitor();
					if (errorFocus) {
						EdgeLogUtils.warn(monitorDescriptor, "目录服务器连接恢复正常");
					}
					errorFocus = false;
				} catch (Throwable e) {
					if (ExceptionUtils.findCause(e, CommunicationException.class) != null
							|| ExceptionUtils.findCause(e, ServiceUnavailableException.class) != null
							|| ExceptionUtils.findCause(e, CannotGetJdbcConnectionException.class) != null) {
						if (!errorFocus) {
							errorFocus = true;
							EdgeLogUtils.error(monitorDescriptor, e);
						}
					} else {
						EdgeLogUtils.error(monitorDescriptor, e);
					}
				}
			}
		}
		setMonitorStatus(MonitorStatus.STOPPED);
	}

	public LdapChangeLogMonitorContext getMonitorContext() {
		return monitorContext;
	}

	void setMonitorContext(LdapChangeLogMonitorContext context) {
		this.monitorContext = context;
	}

	protected String buildChangeLogFilter(LdapChangeLogMonitorDescriptor lcmd, LdapDataEdgeDescriptor mds,
			String lastChangeId) {

		StringBuilder sbf = new StringBuilder("(&(objectclass=");
		sbf.append(lcmd.getChangeLogObjectClass()).append(")(");
		if (StringUtils.hasText(lastChangeId)) {
			sbf.append(lcmd.getChangeLogEntityID()).append(">=").append(lastChangeId).append(")(");
		}
		sbf.append(lcmd.getTargetEntityPropertyName()).append("=*,").append(mds.getEntityParentDN());
		sbf.append("))");
		return sbf.toString();
	}

	protected void doMonitor() throws Exception {
//		Log.debug("Get changelog from LdapContext.");
		LdapChangeLogMonitorContext monitorContext = getMonitorContext();
		ChangeLogResolver changeLogResolver = monitorContext.getChangeLogResolver();
		ChangeLogStorage changeLogStorage = monitorContext.getChangeLogStorage();

		ContextSource ctxSource = monitorContext.getLdapContextSource();

		LdapChangeLogMonitorDescriptor lcmd = monitorContext.getChangeLogMonitorDescriptor();
		DataProperty targetProperty = new DataProperty(lcmd.getTargetEntityPropertyName());
		DataProperty changeTypeProperty = new DataProperty(lcmd.getChangeTypePropertyName());
		DataProperty changedContentProperty = new DataProperty(lcmd.getChangedContentPropertyName());
		DataProperty changeLogIdProperty = new DataProperty(lcmd.getChangeLogEntityID(), Types.BIGINT);
		String changedContextSplit = lcmd.getChangedContextPropertySplit();

		DataProperty dataProps[] = lcmd.getChangeLogProperties().toArray(new DataProperty[0]);

		for (LdapDataEdgeDescriptor ldapDes : monitorContext.getMonitoredDataDescriptors()) {
			if (getMonitorStatus() != MonitorStatus.STARTED)
				break;
			DataChangeLog lastChangeLog = changeLogResolver.getLastChangeLog(ldapDes);
			if (lastChangeLog == null) {
				// 发现尚未初始化的数据，进行数据初始化
				EdgeLogUtils.warn(ldapDes, "第一次运行，开始数据初始化");
				List<Map<DataProperty, String>> firstInitedDatas = resolveLdapDataByTargetDN(
						ctxSource,
						ldapDes,
						new StringBuilder(ldapDes.getEntityPropertyId()).append("=*,")
								.append(ldapDes.getEntityParentDN()).toString(), DataAction.INSERT);
				for (int i = firstInitedDatas.size() - 1; i >= 0; i--) {
					List<Map<DataProperty, String>> tempList = new ArrayList<Map<DataProperty, String>>(1);
					tempList.add(firstInitedDatas.get(i));
					monitorContext.processMonitorData(ldapDes, DataAction.INSERT, tempList);
				}
				String changeLogFilter = buildChangeLogFilter(lcmd, ldapDes, null);
				String lastChangeId = "0";
				List<Map<DataProperty, String>> allChangeLogs = LdapUtils.getEntities(ctxSource,
						lcmd.getChangeLogParentDN(), changeLogFilter, dataProps);
				if (allChangeLogs.size() > 0) {
					lastChangeId = allChangeLogs.get(allChangeLogs.size() - 1).get(changeLogIdProperty);
				}
				changeLogStorage.saveChangeLog(ldapDes, lastChangeId, DataAction.INSERT);
				EdgeLogUtils.warn(ldapDes, "初始化完毕，最后变更记录ID:" + lastChangeId);
			} else {
				Set<String> dataPropNames = ldapDes.getEntityPropertiesNameSet();
				String changeLogFilter = buildChangeLogFilter(lcmd, ldapDes, lastChangeLog.getDataIdentity());
				for (Map<DataProperty, String> changeLog : LdapUtils.getEntities(ctxSource,
						lcmd.getChangeLogParentDN(), changeLogFilter, 0, getRetrieveCount(), dataProps)) {
					if (getMonitorStatus() == MonitorStatus.STOPPED)
						break;
					if (ObjectUtils.nullSafeEquals(changeLog.get(changeLogIdProperty), lastChangeLog.getDataIdentity()))
						continue;
					String targetDN = changeLog.get(targetProperty);
					String changeLogId = changeLog.get(changeLogIdProperty);
					DataAction dataAction = lcmd.getDataActionByNameMap(changeLog.get(changeTypeProperty));
					boolean doLogAction = true;
					if (dataAction == DataAction.UPDATE) {
						doLogAction = false;
						String[] changedContents = changeLog.get(changedContentProperty).split(changedContextSplit);
						if (changedContents.length > 1) {
							for (int i = 0; i < changedContents.length; i = i + 2) {
								int startIndex = changedContents[i].indexOf(":");
								String changedPropertyName = changedContents[i].substring(startIndex + 1).trim();
								if (dataPropNames.contains(changedPropertyName)) {
									doLogAction = true;
								}
							}
						}
					}
					if (doLogAction) {
						monitorContext.processMonitorData(ldapDes, dataAction == DataAction.DELETE ? DataAction.UPDATE
								: dataAction, resolveLdapDataByTargetDN(ctxSource, ldapDes, targetDN, dataAction));
					}
					changeLogStorage.saveChangeLog(ldapDes, changeLogId, dataAction);
				}
			}
			if (getMonitorStatus() != MonitorStatus.STARTED)
				break;
		}
	}

	protected List<Map<DataProperty, String>> resolveLdapDataByTargetDN(ContextSource ctxSource,
			LdapDataEdgeDescriptor targetDataDesc, String targetDN, DataAction action) {
		Assert.notNull(ctxSource);
		Assert.notNull(targetDataDesc);
		Assert.hasText(targetDN);
		Assert.notNull(action);
		int pidIndex = targetDN.indexOf(targetDataDesc.getEntityPropertyId());
		pidIndex = pidIndex == -1 ? 0 : pidIndex + targetDataDesc.getEntityPropertyId().length() + 1;

		int parentDNStartIndex = targetDN.toLowerCase().indexOf(targetDataDesc.getEntityParentDN().toLowerCase());
		Assert.isTrue(parentDNStartIndex >= 0);
		String id = targetDN.substring(pidIndex, parentDNStartIndex - 1);
		List<Map<DataProperty, String>> result = new ArrayList<Map<DataProperty, String>>(1);
		if (action != DataAction.DELETE) {
			String targetSearchFilter = new StringBuilder("(&(objectclass=")
					.append(targetDataDesc.getEntityClassName()).append(")(")
					.append(targetDataDesc.getEntityPropertyId()).append("=").append(id).append("))").toString();
			result = LdapUtils.getEntities(ctxSource, targetDataDesc.getEntityParentDN(), targetSearchFilter,
					targetDataDesc.getEntityProperties().toArray(new DataProperty[0]), action);
		} else {
			Map<DataProperty, String> map = new HashMap<DataProperty, String>(1 + targetDataDesc.getEntityProperties()
					.size());
			for (DataProperty prop : targetDataDesc.getEntityProperties()) {
				String pValue = "";
				if (targetDataDesc.getEntityPropertyId().equalsIgnoreCase(prop.getPropertyName())) {
					pValue = id;
				}
				map.put(prop, pValue);
			}
			map.put(DataProperty.DATA_ACTION, DataAction.DELETE.name());
			result.add(map);
		}
		return result;
	}

	public void stop() {
		this.setMonitorStatus(MonitorStatus.STOPPED);
	}

	public void pause() {
		setMonitorStatus(MonitorStatus.PAUSED);
	}

	public void resume() {
		setMonitorStatus(MonitorStatus.STARTED);
	}

	public void setMonitorStatus(MonitorStatus statu) {
		synchronized (this) {
			monitorStatus = statu;
		}
	}

	public MonitorStatus getMonitorStatus() {
		synchronized (this) {
			return monitorStatus;
		}
	}
}
